﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using System.Reflection;
using System.ComponentModel;
using System.Windows.Input;
using System.Diagnostics;
using System.Windows.Threading;
using System.Linq.Expressions;
using System.Collections.ObjectModel;

///Behavior Injection Framework
///Created by: Lance Wynn 3/12/2011
///Description: This library endeavors to provide a bridging mechanism for mvvm pattern development
///to allow developers to create dynamic ViewModels by merging a DataClass, with Behavior.
///The DataClass takes the form of a POCO object with public getters/setters, the Behavior will inherit from the Behavior injector, and
///supply behavior in the form of ObservableProperties, and Commands.  The property and command wireup also includes automatic dependency
///tracking, and propertychanged notification.

namespace BehaviorFramework
{
	public class BehaviorInjector<T> //: BehaviorExpando
	{
		private Dispatcher _uiDispatcher = Dispatcher.CurrentDispatcher;

		public BehaviorInjector(T dataModel)
		{
			_ViewModel = new BehaviorExpando<T>();
			AttachObservableBehavior(dataModel);
		}

		public BehaviorInjector()
		{
			//Empty constructor to allow for behavior extensions.
		}

		[NonObservable]
		private bool HasMappedDependencies { get; set; }

		private T _DataModel;
		[NonObservable]
		public T DataModel
		{
			get
			{
				//Get enumerable properties so we can reload from the ObservableCollection Wrapper
				foreach (var property in _DataModel.GetType().GetProperties().Where(p=>p.PropertyType.GetIEnumerableType()!=null))
				{
					var collectionProperty = ((BehaviorExpando<T>)ViewModel).propertyDictionary[property.Name]; 
					object collection = collectionProperty.Get.Invoke();
					property.SetValue(_DataModel, collection.AsIEnumerableType(collectionProperty.BaseCollectionType), null);
				}
				return _DataModel;
			}
			private set
			{
				_DataModel = value;
			}
		}

		private dynamic _ViewModel;
		[NonObservable]
		public dynamic ViewModel
		{
			get
			{
				return _ViewModel;
			}
			set
			{
				HasMappedDependencies = false;
				_ViewModel = value;
			}
		}

		private void AttachObservableBehavior(T model)
		{
			DataModel = model;
			_ViewModel.DataModel = model;
			ApplyObservableBehaviorToModel(model);
			ApplyObservableBehaviorToModel(this);
		}

		private void RemapDependencies()
		{
			ViewModel.RemapDependencies();
		}

		public BehaviorInjector<T> AttachBehavior<J>() where J : new()
		{
			if (typeof(J).GetBehaviorInjectorType(DataModel) != typeof(BehaviorInjector<T>))
			{
				throw new Exception("Behavior Types must match");
			}
			J behavior;
			var ctor = typeof(J).GetConstructor(new System.Type[] { typeof(T) });
			if (ctor != null)
			{
				behavior = (J)ctor.Invoke(new object[] { this.DataModel });
			}
			else
			{
				ctor = typeof(J).GetConstructor(new System.Type[] { });
				behavior = (J)ctor.Invoke(null);
			}

			ApplyObservableBehaviorToModel(behavior);
			return this;
		}

		public BehaviorInjector<T> ApplyObservableBehaviorToModel(object model)
		{
			var properties = model.GetType().GetProperties();

			//If the model type is a behavior, then we need to attach the current ViewModel to it so the viewmodel is referenced in all behaviors that may be attached.
			if (model.IsBehaviorInjector<T>() && ((BehaviorInjector<T>)model).ViewModel == null)
			{
				((BehaviorInjector<T>)model).ViewModel = this.ViewModel;
			}

			foreach (var property in properties)
			{
				var isUnobservable = Attribute.GetCustomAttribute(property, typeof(NonObservable));
				if (isUnobservable != null)
				{
					continue;
				}

				var closedProp = property;
				//var objRef = closedProp.GetValue(model, null);

				var iEnumerableType = closedProp.PropertyType.GetIEnumerableType();
				ObservableProperty observableProperty;

				if (iEnumerableType == null)
				{
					observableProperty = new ObservableProperty(() => closedProp.GetValue(model, null),
						(value) =>
						{
							closedProp.SetValue(model, value, null);
						}, property.PropertyType);
				}
				else
				{
					var originalList = closedProp.GetValue(model, null);
					var observableCollection = originalList.AsObservableCollection(iEnumerableType);

					Type baseCollectionType;
					if (originalList != null)
					{
						baseCollectionType = originalList.GetType();
					}
					else
					{
						baseCollectionType = closedProp.PropertyType;
					}

					observableProperty = new ObservableProperty(() => observableCollection,
						(value) =>
						{
							observableCollection = value;
						}, observableCollection.GetType());
					observableProperty.IsCollectionType = true;
					observableProperty.BaseCollectionType = baseCollectionType;

				}
				_ViewModel.AddObservableProperty(closedProp.Name, observableProperty);
			}

			var methods = model.GetType().GetMethods();
			foreach (var method in methods)
			{
				var isUnobservable = Attribute.GetCustomAttribute(method, typeof(NonObservable));
				if (isUnobservable != null)
				{
					continue;
				}

				var UseCommandBehavior = (UseCommandBehavior)Attribute.GetCustomAttribute(method, typeof(UseCommandBehavior));
				if (UseCommandBehavior == null)
				{
					continue;
				}

				var canExecuteMethodName = UseCommandBehavior.CanExecuteMethodName;
				if (string.IsNullOrEmpty(canExecuteMethodName))
				{
					canExecuteMethodName = "Can" + method.Name;
				}

				if (method.GetParameters().Count() <= 1) //if the method has 1 or less parameters, it is a candidate for a command behavior.
				{
					var closedMethod = method;
					var command = new BehaviorCommand((p) =>
					{
						var param = new object[] { p };
						if (closedMethod.GetParameters().Count() == 1)
						{
							closedMethod.Invoke(model, param);
						}
						else
						{
							closedMethod.Invoke(model, null);
						}
					});

					if (!string.IsNullOrEmpty(canExecuteMethodName))
					{
						var canExecuteMethod = model.GetType().GetMethods().Where(m => m.Name == canExecuteMethodName).FirstOrDefault();
						if (canExecuteMethod != null)
						{
							command.CanExecuteFunc = (p) =>
							{
								var param = new object[] { p };
								if (canExecuteMethod.GetParameters().Count() == 1)
								{
									return (bool)canExecuteMethod.Invoke(model, param);
								}
								else
								{
									return (bool)canExecuteMethod.Invoke(model, null);
								}
							};
						}
					}

					_ViewModel.AddBehaviorCommand(method.Name, command);
				}
			}
			RemapDependencies();
			return this;
		}
	}

	public static class BehaviorInjectionExtensions
	{
		/// <summary>
		/// traverse through the inheritance tree to fund out if the referenced object inherits from BehaviorInjector.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="model"></param>
		/// <returns></returns>
		public static bool IsBehaviorInjector<T>(this object model)
		{
			var typeToCheck = model.GetType();

			while (typeToCheck != typeof(object))
			{
				if (typeof(BehaviorInjector<T>) == typeToCheck)
				{
					return true;
				}
				typeToCheck = typeToCheck.BaseType;
			}
			return false;
		}

		public static Type GetBehaviorInjectorType<T>(this Type type, T model)
		{
			var typeToCheck = type;

			while (typeToCheck != typeof(object))
			{
				if (typeof(BehaviorInjector<T>) == typeToCheck)
				{
					return typeof(BehaviorInjector<T>);
				}

				typeToCheck = typeToCheck.BaseType;
			}
			return null;
		}

		public static Type GetIEnumerableType(this Type type)
		{
			if (type.IsGenericType)
			{
				var genericTypeDefinition = type.GetGenericTypeDefinition();

				if (genericTypeDefinition.GetInterfaces().Any(t => t == typeof(System.Collections.IEnumerable) || t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
				{
					return type.GetGenericArguments()[0];
				}
			}
			return null;
		}


		//Convert List to observable collection..
		public static object AsObservableCollection(this object items, Type type = null)
		{
			if (type == null)
			{
				type = items.GetType().GetIEnumerableType();
			}
			
			Type genericClass = typeof(ObservableCollection<>);
			// MakeGenericType is badly named
			Type constructedClass = genericClass.MakeGenericType(type);
			object createdObject ;
			if (items == null)
			{
				createdObject = Activator.CreateInstance(constructedClass);
			}
			else
			{
				createdObject = Activator.CreateInstance(constructedClass, items);
			}
			
			return createdObject;
		}

		public static object AsIEnumerableType(this object observableCollection, Type originalEnumerableType)
		{
			Type typeArgument = originalEnumerableType;

			if (originalEnumerableType.IsInterface)//if it's an interface, then it means the value was null, and an underlying type was not defined, so we create it as a list<T>
			{
				typeArgument = typeof(List<>).MakeGenericType(originalEnumerableType.GetIEnumerableType());
			}


			var createdObject = Activator.CreateInstance(typeArgument, observableCollection);

			return createdObject;
		}

		public static T AttachBehavior<T>(this object dataModel) where T : new()
		{
			return (T)typeof(T).GetConstructor(new System.Type[] { dataModel.GetType() }).Invoke(new object[] { dataModel });
		}
	}
}
